LK

Synchronized Ready-Up System

Finished
GitHub

This system enables player state synchronization in the form of a ready-up mechanic. It ensures that certain gameplay actions are only triggered once all players have explicitly confirmed their readiness.

For player state synchronization to function reliably, the server must be able to uniquely identify each connected player. This is achieved by mapping every client to a unique identifier upon login. The GameMode is the ideal place for this logic, as it runs exclusively on the server and provides built-in networking events for client login and logout.

/*
 * Gets called on Client login
 */
void ASDGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	ConnectedClients.Add(NewPlayer);

	FConnectedClientData NewClientData = FConnectedClientData();
	NewClientData.ClientController = NewPlayer;
	NewClientData.ClientName = GetNewPlayerName();
	NewClientData.bIsReady = false;
	// Map a client to a unique identifier, a name in this case
	ConnectedClientsDataByController.Add(NewPlayer, NewClientData);

	// Update to all other clients, that a new client has joined
	UpdateClientsReadyStatus();

	// Check if the game is already over and if the ready up screen needs to be loaded
	CheckIfGameOver(NewPlayer);
}

/*
 * Gets called on Client logout
 */
void ASDGameModeBase::Logout(AController* ExitingPlayer)
{
	Super::Logout(ExitingPlayer);

	// return if server
	if (ExitingPlayer->IsLocalPlayerController())
		return;
	
	ConnectedClients.Remove(ExitingPlayer);
	ConnectedClientsDataByController.Remove(ExitingPlayer);
	
	// Update to all other clients, that a client has left
	UpdateClientsReadyStatus();
}

Handling late-joining players is inherently challenging, as it cannot be guaranteed that a newly connected client has finished loading all necessary data. In this implementation, the client must first receive game-specific data from the GameState. To ensure this, the server periodically checks the client’s readiness via a per-client timer handle.

/*
* Checks whether the game has ended and, if so, triggers the ready-up check for the corresponding PlayerController
* Periodically verifies that all required game data is present on the client
*/
void ASDGameModeBase::CheckIfGameOver(AController* ClientController)
{
	if (GetSDGameState())
	{
		if (USDGameplayLibrary::GetIsGameOver(this))
		{
			if (FConnectedClientData* ClientData = ConnectedClientsDataByController.Find(ClientController))
			{
				ClientData->bNeedsLateJoinCheck = true;
				GetWorld()->GetTimerManager().SetTimer(ClientData->LateJoinCheckGameOverTimer,
					[this, ClientController]()
					{
						CheckForClientGameState(ClientController);
					},
					ClientLateJoinNotifyTimer, true);
			}
		}
	}
}

The actual updating and synchronization of all players’ ready-up states is handled by a function that is executed whenever a player changes their status. This function iterates over all connected client controllers and invokes a ClientRPC to update each client.

/*
 * Requests the current ready status of all clients and updates them accordingly
 */
void ASDGameModeBase::UpdateClientsReadyStatus()
{
	if (ConnectedClients.Num() == 0)
		return;

	TArray<FConnectedClientData> ConnectedClientsData;
	ConnectedClientsDataByController.GenerateValueArray(ConnectedClientsData);
	
	for (AController* Controller : ConnectedClients)
	{
		if (ASDPlayerController* SDPlayerController = Cast<ASDPlayerController>(Controller))
		{
			SDPlayerController->Client_SendClientsReadyStatus(ConnectedClientsData);
		}
	}

	if (AllClientsReady())
		StartRestartProcess();
	else
		EndRestartProcess();	
}

To allow players to control their ready-up status via the user interface, we expose a Blueprint-callable function. When the ready button is pressed, this function sends the new status to the server. The call is executed on the PlayerController, as the GameMode can easily identify the player through its associated controller.

void ASDPlayerController::Server_SetClientIsReady_Implementation(AController* ClientController, const bool bIsReady)
{
	if (ASDGameModeBase* SDGameMode = Cast<ASDGameModeBase>(GetWorld()->GetAuthGameMode()))
		SDGameMode->SetClientIsReady(ClientController, bIsReady);
}

The corresponding setter function inside the GameMode updates the client’s status and subsequently triggers a broadcast with the latest payload of client data to all connected clients.


void ASDGameModeBase::SetClientIsReady(AController* ClientController, bool bIsReady)
{
	if (!IsValid(ClientController))
		return;
	
	if (FConnectedClientData* ClientData = ConnectedClientsDataByController.Find(ClientController))
	{
		ClientData->bIsReady = bIsReady;
		UpdateClientsReadyStatus();
	}
}

Whenever a new ready-up status is requested, the system immediately broadcasts the updated client data payload to all connected clients. This ensures that every client receives the latest state without delay and remains fully synchronized.

// .h ClientData sync Delegate Signature
DECLARE_MULTICAST_DELEGATE_OneParam(FOnConnectedClientsDataChanged, const TArray<FConnectedClientData>& NewConnectedClientData);

void ASDPlayerController::Client_SendClientsReadyStatus_Implementation(const TArray<FConnectedClientData>& ConnectedClientsData)
{
	OnClientsDataChanged.Broadcast(ConnectedClientsData);
}

The system leverages the Model-View-Controller (MVC) pattern. From within the controller class, another broadcast is invoked to forward the updated data to the client’s UI. For a detailed overview of the MVC pattern in use, please refer to the Inventory System page. Once the HUD is initialized, the ready-up logic is bound to the UI.

The user interface simply generates a list of ReadyUpStatusItems that reflect the state of all connected players.

If you are interested, the project’s full source code can be found directly below the video at the beginning of this page.